【JavaScript】JavaScript高级程序设计 第六章 面向对象的程序设计

面向对象的语言的标志:有类的概念,通过类创建任意多个具有相同属性和方法的对象。

ECMAScript没有类的概念,因此它的对象和基于类语言的对象有所不同。

ECMAScript的对象:“散列表” 一组名值对,值可以使数据或者函数。基于引用类型创建,这个引用类型可以是原生类型,也可以是自定义的类型。

FVe90g.png

6.1理解对象

创建自定义对象的方法:①创建一个object实例,再为它添加属性和方法 ②对象字面量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/*创建对象*/
//创建object实例,再为它添加属性和方法
var person = new Object();
person.name = 'JoseyDon';
person.age = 20;
person.sayName = function(){
alert(this.name);
}
//对象字面量
var person = {
name:'JoseyDon',
age:20,
sayName: function(){
alert(this.name);
}
}

6.1.1属性类型

ECMAScript中有两种属性:①数据属性 ②访问器属性

①数据属性:包含一个数据值的位置,在这个位置可以读取和写入值。有四个特性(描述该属性行为)。

特性 作用 (直接在对象上定义的属性)默认值
[[Configurable]] 能否通过delete删除属性从而重新定义属性,能否修改属性的特性,能否把属性修改为访问器属性 true
[[Enumerable]] 能否通过for-in循环返回属性 true
[[Writable]] 能否修改属性的值 true
[[Value]] 包含这个属性的数据值 undefined(任何)

修改属性默认的特性:Object.defineProperty(属性所在对象,属性名字,描述符对象);其中描述符对象的属性必须是configurable、enumerable、writable和value。

在调用该方法创建一个新属性时,如果不指定,configurable、enumerable、writable的值都是false。

②访问器属性:不包含数据值;包含一对不必需的getter和setter函数;有四个特性。

特性 作用 默认值
[[Configurable]] 能否通过delete删除属性从而重新定义属性,能否修改属性的特性,能否把属性修改为访问器属性 (直接在对象上定义的属性)true
[[Enumerable]] 能否通过for-in循环返回属性 true
[[Get]] 在读取属性时调用的函数 undefined
[[Set]] 在写入属性时调用的函数 undefined

访问器属性不能直接定义,必须使用Object.defineProperty()来定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/*修改属性默认的特性*/
/*数据属性*/
//第三个参数必须是包含configurable、enumerable、writable和value其中一个或多个的描述符对象
var person = {};
Object.defineProperty(person,"name",{
writable: false,
value: "Jsoeydon"
});
//一旦把属性定义为不可配置,就不能再把它变回可配置的了
Object.defineProperty(person,"name",{
configurable: false,
value: "Jsoeydon"
});
/*访问器属性*/
var book = {
_year: 2004,
edition: 1
};
Object.defineProperty(book,'year',{
get: function(){
return this._year;
},
set:function(newValue){
if (newValue>2004) {
this._year = newValue;
this.edition += newValue - 2004;
}
}
})
//_year前面的下划线是一种常用的记号,用于表示只能通过对象方法访问的属性。
//使用访问器属性的常见方式:设置一个属性的值会导致其他属性发生变化。
//只指定getter:属性不能写 严格模式下不能只指定getter
//只指定setter:属性不能读 非严格模式返回undefined 严格模式下出错

6.1.2定义多个属性

Object.defineProperties(要改变其属性的对象,属性):通过描述符一次定义多个属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/*定义多个属性*/
var book = {};
Object.defineProperties(book,{
_year: {
writable: true,
value:2004
},
edition: {
writable: true,
value: 1
},
year: {
get: function(){
return this._year;
},
set: function(newValue){
if (newValue>2004) {
this._year = newValue;
this.edition += newValue -2004;
}
}
}
})

6.1.3读取属性的特性

Object.getOwnPropertyDescriptor(属性所在的对象,属性名称):取得给定属性的描述符;返回值是一个对象。

6.2创建对象

FVZv1P.png

6.2.1工厂模式

用函数来封装以特定接口创建对象的细节

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/*工厂模式*/
function createPerson(name,age){
var o = new Object();
o.name = name;
o.age = age;
o.sayName = function(){
alert(this.name);
};
return o;
}
var person1 = createPerson('JoseyDon',20);
var person2 = createPerson('xiaohong',19);

6.2.2构造函数模式

像Object和Array这样的原生构造函数,在运行时会自动出现在执行环境中。

也可以创建自定义的构造函数,从而定义自定义对象类型的属性和方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
/*构造函数模式*/
function Person(name,age){
this.name = name;
this.age = age;
this.sayName = function(){
alert(this.name);
}
}
var person1 = new Person("JoseyDon",20);
var person2 = new Person("xiaohong",19);

与工厂模式的区别:1.没有显示地创建对象 2.直接将属性和方法赋给了this对象 3.没有return语句

惯例:构造函数始终都以一个大写字母开头,非构造函数则以一个小写字母开头

用构造函数创建对象经历的步骤:1.创建一个新对象 2.将构造函数的作用域赋给新对象(this指向新对象)3.执行构造函数中的代码(为对象添加属性和方法) 4.返回新对象

上述代码中,创建的两个对象都有construction(构造函数)属性,该属性指向Person。

这两个对象既是Object的实例,又是Person的实例。(通过instanceof操作符检测数据类型)

创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的属性。

构造函数和其他函数的唯一区别在于调用它们的方式不同。所以构造函数也可以当做普通函数来使用。普通函数通过new操作符来调用就可以作为构造函数。

构造函数创建对象的缺点:每个方法都要在每个实例上重新创建一遍。

6.2.3原型模式

创建的每个函数都有一个prototype(原型)属性,这个属性是个指向一个对象的指针,这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。

prototype就是通过构造函数而创建的那个对象实例的原型对象。

这种模式创建的对象实例可以共享它所包含的属性和方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/*原型模式*/
function Person(){}
Person.prototype.name = "JoseyDon";
Person.prototype.age = 20;
Person.prototype.sayName = function(){
alert(this.name);
};
var person1 = new Person();
person1.sayName(); //JoseyDon
var person2 = new Person();
person2.sayName();//JoseyDon
alert(person1.sayName == person2.sayName);//true
//person1和person2访问的都是同一组属性和同一个sayName()函数

原型对象:

FVZx6f.png

读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性。

对象实例——原型对象

通过对象实例访问保存在原型中的值,但不能通过对象实例重写原型中的值。

在实例中添加一个属性,而该属性与实例原型中的一个属性同名,那么就会在实例中创建该属性,该属性将会屏蔽原型中的那个属性。

添加与原型对象同名的属性只会阻止我们访问原型中的那个属性,但不会修改那个属性。

使用delete操作符可以完全删除实例属性。

检查属性存在于实例中还是原型中:

obj.hasOwnProperty(“属性名”) //只有属性存在于实例对象时,才会返回true

function hasPrototypeProperty(object,name){

​ return !object.hasOwnProperty(name)&&(name in object);

}//判断属性是否存在于原型对象中

原型与in操作符:

in操作符:通过对象能够访问给定属性时返回true,无论属性存在于实例对象还是原型对象中

for-in循环:返回的是所有能够通过对象访问的、可枚举的属性。无论属性存在于实例对象还是原型对象中。屏蔽原型中不可枚举的属性。

for-in循环会将实例对象中[[Enumerable]]为false的属性也访问到,要获取对象上所有可枚举的实例属性,Object.keys(对象)方法,返回包含所有可枚举属性的字符串数组。

想获得所有实例属性,使用Object.getOwnPropertyName()

重写原型对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//重写原型对象
function Person(){};
Person.prototype = {
name: 'JoseyDon',
age: 20,
sayName: function(){
alert(this.name);
}
};
//constructor属性不再指向Person,可以特意将它设置回来
function Person(){};
Person.prototype = {
constructor: Person,
name: 'JoseyDon',
age: 20,
sayName: function(){
alert(this.name);
}
};
//这种方式会导致它的[[Enumerable]]特性被设置为true

原型的动态性:

由于在原型中查找值的过程是一次搜索,因此原型对象的修改能够立即从实例上反映出来。

原生对象的原型:

除了创建自定义类型,所有原生的引用类型(Object、Array、String等等)都是采用原生对象的模型创建的。

原生对象的问题:

所有实例在默认情况下都将取得相同的属性值。

6.2.4组合使用构造函数模式和原型模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*组合使用构造函数模式和原型模式*/
/*构造函数模式:定义实例属性(默认属性值可以自定义)
原型模式:定义方法和共享的属性(不用多次创建相同的函数)*/
function Person(name,age){
this.name = name;
this.age = age;
this.friends = ["xiaohong","xiaolv"];
}
Person.prototype = {
constructor: Person,
sayName: function(){
alert(this.name);
}
}

6.2.5动态原型模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/*动态原型模式*/
/*通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型*/
function Person(name,age){
//属性
this.name = name;
this.age = age;
//方法
if (typeof this.sayName != "function") {
Person.prototype.sayName = function(){
alert(this.name);
};
}
}
//只在sayName()方法不存在的情况下,才会将它添加到原型中。
//这段代码只会再初次调用够构造函数时才会执行。

6.2.6寄生构造函数模式
除了使用new操作符并把使用的包装函数叫做构造函数外,这个模式跟工厂模式其实是一模一样的。

6.2.7稳妥构造函数模式

稳妥对象:没有公共属性,而其方法也不引用this的对象。

6.3继承

6.3.1原型链

基本思想:利用原型让一个引用类型继承另一个引用类型的属性和方法。

实现原理:让原型对象等于另一个类型的实例。

FVZzX8.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/*原型链*/
//父类
function SuperType(){
this.property = true;
}
//父类的原型对象的函数
SuperTyper.prototype.getSuperValue = function(){
return this.property;
}
function SubType(){
this.subproperty = false;
}
//继承
SubType.prototype = new SubType();
SubType.prototype.getSubValue = function(){
return this.subproperty
}
var instance = new SubType(){
alert(instance.getSuperValue());
}

在通过原型链实现继承的情况下,搜索过程就会沿着原型链继续向上:1)搜索SubType实例 2)搜索SubType.prototype 3)搜索SuperType.prototype

注意事项:①所有引用类型默认都继承了Object,这个继承也是通过原型链实现的

​ ②确定原型和实例的关系 1)instancepf操作符 2)isPrototypeOf()方法

​ ③子类型在覆盖超类型中的某个方法或者添加超类型中不存在的方法时,给原型添加方法的代码一定要放在替换原型的语句之后。

​ ④通过原型链实现继承时,不能使用对象字面量创建原型方法。

原型链的问题:①所有属性将会被共享

​ ②不能再不影响所有对象实例的情况下,给超类型的构造函数传递参数,

6.3.2借用构造函数

借用构造函数/伪造对象/经典继承

基本思想:在子类型构造函数的内部调用超类型构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/*借用构造函数*/
//父类
function SuperType(){
this.colors = ["red","blue","green"];
}
//子类
function SubType(){
//继承了SuperType()
SubType.call(this);//使用call()调用父类函数//apply()也可以
}
var instance1 = new SubType();
instance1.colors.push("black");
alert(oinstance1.colors);//"red,blue,green,black"
alert(instance1.colors);
var instance2 = new SubType();
alert(instance2.colors);//"red,blue,green"//证明不是公用一个colors属性
/*在新创建的SubType实例的环境下调用SuperType构造函数,
这样就会在新SubType对象上执行SuperType()函数中定义的所有对象的初始化代码。
结果SubType的每个实例都会具有属于自己的colors属性的副本了。*/
//在子类型构造函数中向超类型构造函数传递参数
function SuperType(name){
this.name = name;
}
function SubType(){
//继承了超类型,还传递了参数
SuperType.call(this,"JoseyDon");
//实例属性
this.age = 20;
}
var instance = new SubType();
alert(instance.name);
alert(instance.age);
//为了确保父类构造函数不会重写子类型的属性,
//可以在调用超类型构造函数后,再添加应该在子类型中定义的属性

借用构造函数的问题:没有做到函数的复用。

6.3.3组合继承

组合继承/伪经典继承:将原型链和借用构造函数的技术组合到一块。

基本思想:使用原型链实现对原型属性和方法的继承,使用借用构造函数实现对实例属性的继承。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/*组合继承*/
//父类
function SuperType(name){
this.name = name;
this.colors = ["red","blue","green"];
}
SuperType.prototype.sayName = function(){
alert(this.name);
}
function SubType(name,age){
//借用构造函数继承属性
SuperType.call(this,name);
this.age = age;
}
//原型链 继承方法
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
alert(this.age);
}
var instance1 = new SubType("JoseyDon",20);
instance1.colors.push("black");
alert(instance1.colors);//"red,blue,green,black"
instance1.sayName();//JoseyDon
instance1.sayAge();//20
var instance2 = new SubType("xiaohong",21);
alert(instance2.colors);//"red,blue,green"
instance2.sayName();//xiaohong
instance2.sayAge();//21

6.3.4原型式继承

6.3.5寄生式继承

6.3.6寄生组合式继承